vlwkaos' digital garden

React Component Library from scratch - Bundle and Publish

라이브러리처럼 작동하게 만들기 위해 다른 프로젝트에서 접근할 수 있도록 접근 포인트를 지정해줘야한다. package.jsonmain필드 값을 lib/index.js로 지정해주자. 번들러를 통해 생성된 파일 경로가 될 것이다.

그리고 파일 하나를 더 추가해야하는데, 이 파일은 우리가 만든 컴포넌트 라이브러리에서 어떤 부분이 노출이 되는지 결정한다. src/index.js 을 만들고 만들었던 버튼 컴포넌트를 export해보자

export {default as Button } from "components/Button";

방금 만든 src/index.js파일과 lib/index.js파일은 각각 우리가 만드는 라이브러리의 입/출력이라고 볼 수 있다.

번들

Rollup을 이용하여 번들링 할 것이다. 설치해주자

npm i -D rollup rollup-plugin-commonjs rollup-plugin-babel

Rollup 설정 파일을 만들자. rollup.config.js

import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";

import packageJSON from "./package.json";
const input = "./src/index.js";

export default [
  // CommonJS
  {
    input,
    output: {
      file: packageJSON.main,
      format: "cjs"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      commonjs()
    ]
  }
];

Rollup설정 파일을 보면 inputoutput 필드가 각각 src/index.jslib/index.js를 번들링 하면 된다고 경로를 지정해주는 부분이다.

이제 package.json에 빌드 스크립트를 추가하자.

"build": "rollup -c"

그냥 빌드를 해보면 경고 메세지가 두개 뜬다.

(!) Unresolved dependencies
...
(!) Unused external imports

Rollup은 상대경로의 모듈 ID만 해결한다. 그러니까 import X from 'Y'같은 import구문은 제대로 작동하지 않는다는 뜻이다. 이런 것들은 runtime시 포함되는 외부 dependency로 취급한다. 물론 유저가 Y모듈을 자기 컴퓨터에 설치해 뒀다면 작동하겠지만 라이브러리 특성상 사용자가 설치하게 끔 하는 것은 올바른 방향은 아니다.

모듈 해결

다음 플러그인을 설치한다.

npm i -D rollup-plugin-node-resolve

롤업 설정 파일에 추가하자

import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import resolve from "rollup-plugin-node-resolve"; // 불러와서

import packageJSON from "./package.json";
const input = "./src/index.js";

export default [
  // CommonJS
  {
    input,
    output: {
      file: packageJSON.main,
      format: "cjs"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      resolve(), // 여기 추가
      commonjs()
    ]
  }
];

이렇게 하고 npm run build를 하면 아직 외부 라이브러리를 불러올 수 없어서 에러가 발생한다.

PeerDependencies

프로젝트를 생성할 때 React와 Emotion을 peerDependencies로 추가했었다. 이게 의미하는 바는 의도적으로 사용자가 직접 설치해야 함을 의미한다. 이런 모듈은 번들에 끼워넣기에는 알맞지 않기 때문에 그렇게 한 것이다.

npm i -D rollup-plugin-peer-deps-external

이 플러그인을 설치하면 package.json에 정의된 peerDependencies를 자동으로 Rollup external설정에 추가해준다. Rollup 설정파일을 다음처럼 바꿔주자

import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import resolve from "rollup-plugin-node-resolve";
import external from "rollup-plugin-peer-deps-external"; // 불러온다

import packageJSON from "./package.json";
const input = "./src/index.js";

export default [
  // CommonJS
  {
    input,
    output: {
      file: packageJSON.main,
      format: "cjs"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(), // 추가
      resolve(),
      commonjs()
    ]
  }
];

이제 제대로 빌드가 될 것이다. 이 상태에서 publish한 경우 프로젝트 이름으로 import하여 만들어 놨던 버튼을 참조가 가능하다.

로컬에서 사용하기

변경점이 있을 때마다 계속 다시 빌드하고 publish하기는 번거롭다. 그럴 때는 link라는 커맨드를 사용한다. 라이브러리 프로젝트 폴더가 ~/ComponentLibrary라고 하자. 그리고 다른 프로젝트에서 이를 사용하고자 할때 이런식으로 사용할 수 있다.

링크할 프로젝트를 지정한다.

cd ~/ComponentLibrary
yarn link

어디에서 사용할 건지 지정한다.

cd ~/project
yarn link "프로젝트 이름"

이렇게 하면 만들어진 번들 파일을 원하는 프로젝트에서 사용 가능하다. 만들어진 번들 파일을 사용하기 때문에 라이브러리에 변화가 생겼다면 다시 빌드 해야한다.

번들 간소화

rollup-plugin-uglify는 Rollup 2버전과 호환이 되지 않는다. rollup-plugin-terser를 이용한다.

npm i -D rollup-plugin-terser
import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import resolve from "rollup-plugin-node-resolve";
import external from "rollup-plugin-peer-deps-external";
import { terser } from "rollup-plugin-terser";

import packageJSON from "./package.json";
const input = "./src/index.js";
const minifyExtension = pathToFile => pathToFile.replace(/\.js$/, ".min.js");

export default [
  // CommonJS
  {
    input,
    output: {
      file: packageJSON.main,
      format: "cjs"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs()
    ]
  },
  {
    input,
    output: {
      file: minifyExtension(packageJSON.main),
      format: "cjs"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs(),
      terser()
    ]
  }
];

다른 모듈 시스템 사용하기

모듈 시스템에 대해서는 [[JavaScript Module Systems]]를 참조.

다양한 모듈 시스템에 호환을 제공해 주기 위해 여러 버전으로 빌드할 수 있다.

package.json을 다음처럼 바꿔주자.

{  
  ...  
  "main": "lib/index.js",  
  "browser": "lib/index.umd.js",  
  "module": "lib/index.es.js",  
  ...  
}

rollup.config.js에 UMD, ES 용 내용 추가

import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import resolve from "rollup-plugin-node-resolve";
import external from "rollup-plugin-peer-deps-external";
import { terser } from "rollup-plugin-terser";

import packageJSON from "./package.json";
const input = "./src/index.js";
const minifyExtension = pathToFile => pathToFile.replace(/\.js$/, ".min.js");

export default [
  // CommonJS
  {
    input,
    output: {
      file: packageJSON.main,
      format: "cjs"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs()
    ]
  },
  {
    input,
    output: {
      file: minifyExtension(packageJSON.main),
      format: "cjs"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs(),
      terser()
    ]
  },
  // UMD
  {
    input,
    output: {
      file: packageJSON.browser,
      format: "umd",
      name: "reactSampleComponentsLibrary",
      globals: {
        react: "React",
        "@emotion/styled": "styled",
        "@emotion/styled/base": "styled-base",
        "@emotion/react": "css"
      }
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs()
    ]
  },
  {
    input,
    output: {
      file: minifyExtension(packageJSON.browser),
      format: "umd",
      name: "reactSampleComponentsLibrary",
      globals: {
        react: "React",
        "@emotion/styled": "styled",
        "@emotion/styled/base": "styled-base",
        "@emotion/react": "css"
      }
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs(),
      terser()
    ]
  },
  // ES
 {
    input,
    output: {
      file: packageJSON.module,
      format: "es",
      exports: "named"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs()
    ]
  },
  {
    input,
    output: {
      file: minifyExtension(packageJSON.module),
      format: "es",
      exports: "named"
    },
    plugins: [
      babel({
        exclude: "node_modules/**"
      }),
      external(),
      resolve(),
      commonjs(),
      terser()
    ]
  }
];

Publish

package.json에 다음 스크립트를 추가하자.

"prepublishOnly": "rm -rf lib && npm run build",
"postbuild": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz"

첫번째 스크립트는 빌드 폴더를 지우고 다시 빌드 한다. 두번째 스크립트는 어떤 파일이 NPM에 publish될 것인지 보여준다.

참고로 package.jsonname필드는 고유해야한다.

이제 npm에 퍼블리싱 해보자

npm login

정보를 입력하고 로그인 한다.

npm publish

모든게 제대로 되었다면 npmjs.com/package/<패키이지름> 으로 게시되었을 것이다.

그런데 파일 목록을 보면 프로젝트 폴더 내 모든 파일이 등록되어있다. 우리는 번들된 파일만 보여주면 된다.

package.jsonfiles 옵션을 추가하자

{
  ...
  "files": [
    "/lib"
  ],
  ...
}

마지막으로 필요한 경우 Rollup 설정에 output.sourcemaptrue를 설정하여 소스맵을 추가하자.

@emotion/styled를 사용할 때 참고

일부 Emotion기능을 위해서는 babel-plugin-emotion이 필요할 수 있다.

[[React Component Library from scratch - CI]]

React Component Library from scratch - Bundle and Publish